1 /* 2 * Hunt - a framework for web and console application based on Collie using Dlang development 3 * 4 * Copyright (C) 2015-2017 Shanghai Putao Technology Co., Ltd 5 * 6 * Developer: HuntLabs 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.http.cookie; 13 14 import std.regex : regex, Regex; 15 import std.string; 16 import std.conv; 17 18 static import std.algorithm; 19 import core.stdc.stdlib; 20 import std.stdio; 21 22 // XXX VOLVER A PROBAR, HE CAMBIADO TODO LO DE REGEX! 23 24 // Inspired by Python's Cookie.py but Cookie objects are a little different; this 25 // class only stores one cookie per object, not several like the Python version. 26 // Use parse_cookie_header to get a Cookie[] with all the cookies found in a 27 // string 28 29 class CookieException : Exception 30 { 31 this(string msg, string file = __FILE__, size_t line = __LINE__) 32 { 33 super(msg, file, line); 34 } 35 } 36 37 // Used for encoded cookie values matching 38 Regex!char _OctalPatt; 39 Regex!char _QuotePatt; 40 string[string] RESERVED_PARAMS = null; 41 42 static this() 43 { 44 _OctalPatt = regex(r"\\[0-3][0-7][0-7]"); 45 _QuotePatt = regex(r"[\\]."); 46 47 RESERVED_PARAMS = ["expires" : "expires", "path" : "Path", 48 "comment" : "Comment", "domain" : "Domain", "max-age" : "Max-Age", 49 "secure" : "secure", "httponly" : "httponly", "version" : "Version"]; 50 } 51 52 // Chars not needing quotation (fake set for fast lookup) 53 enum _legalchars = ['a' : 0, 'b' : 0, 'c' : 0, 'd' : 0, 'e' : 0, 'f' : 0, 'g' : 0, 54 'h' : 0, 'i' : 0, 'j' : 0, 'k' : 0, 'l' : 0, 'm' : 0, 'n' : 0, 'o' : 0, 55 'p' : 0, 'q' : 0, 'r' : 0, 's' : 0, 't' : 0, 'u' : 0, 'v' : 0, 'w' : 0, 56 'x' : 0, 'y' : 0, 'z' : 0, 'A' : 0, 'B' : 0, 'C' : 0, 'D' : 0, 'E' : 0, 57 'F' : 0, 'G' : 0, 'H' : 0, 'I' : 0, 'J' : 0, 'K' : 0, 'L' : 0, 'M' : 0, 58 'N' : 0, 'O' : 0, 'P' : 0, 'Q' : 0, 'R' : 0, 'S' : 0, 'T' : 0, 'U' : 0, 59 'V' : 0, 'W' : 0, 'X' : 0, 'Y' : 0, 'Z' : 0, '0' : 0, '1' : 0, '2' : 0, 60 '3' : 0, '4' : 0, '5' : 0, '6' : 0, '7' : 0, '8' : 0, '9' : 0, '!' : 0, 61 '#' : 0, '$' : 0, '%' : 0, '&' : 0, '\'' : 0, '*' : 0, '+' : 0, '-' : 0, 62 '.' : 0, '^' : 0, '_' : 0, '`' : 0, '|' : 0, '~' : 0]; 63 64 // Hash for quickly translating chars not in _legalchars 65 // The "," and ";" are encoded for compatibility with some 66 // Safari & Explorer versions 67 enum _cookiechars = [ 68 std.conv.octal!0 : "\\000", std.conv.octal!1 : "\\001", 69 std.conv.octal!2 : "\\002", std.conv.octal!3 : "\\003", 70 std.conv.octal!4 : "\\004", std.conv.octal!5 : "\\005", 71 std.conv.octal!6 : "\\006", std.conv.octal!7 : "\\007", 72 std.conv.octal!10 : "\\010", std.conv.octal!11 : "\\011", 73 std.conv.octal!12 : "\\012", std.conv.octal!13 : "\\013", 74 std.conv.octal!14 : "\\014", std.conv.octal!15 : "\\015", 75 std.conv.octal!16 : "\\016", std.conv.octal!17 : "\\017", 76 std.conv.octal!20 : "\\020", std.conv.octal!21 : "\\021", 77 std.conv.octal!22 : "\\022", std.conv.octal!23 : "\\023", 78 std.conv.octal!24 : "\\024", std.conv.octal!25 : "\\025", 79 std.conv.octal!26 : "\\026", std.conv.octal!27 : "\\027", 80 std.conv.octal!30 : "\\030", std.conv.octal!31 : "\\031", 81 std.conv.octal!32 : "\\032", std.conv.octal!33 : "\\033", 82 std.conv.octal!34 : "\\034", std.conv.octal!35 : "\\035", 83 std.conv.octal!36 : "\\036", std.conv.octal!37 : "\\037", 84 85 std.conv.octal!54 : "\\054", std.conv.octal!73 : "\\073", 86 87 std.conv.octal!177 : "\\177", std.conv.octal!200 : "\\200", 88 std.conv.octal!201 : "\\201", std.conv.octal!202 : "\\202", 89 std.conv.octal!203 : "\\203", std.conv.octal!204 : "\\204", 90 std.conv.octal!205 : "\\205", std.conv.octal!206 : "\\206", 91 std.conv.octal!207 : "\\207", std.conv.octal!210 : "\\210", 92 std.conv.octal!211 : "\\211", std.conv.octal!212 : "\\212", 93 std.conv.octal!213 : "\\213", std.conv.octal!214 : "\\214", 94 std.conv.octal!215 : "\\215", std.conv.octal!216 : "\\216", 95 std.conv.octal!217 : "\\217", std.conv.octal!220 : "\\220", 96 std.conv.octal!221 : "\\221", std.conv.octal!222 : "\\222", 97 std.conv.octal!223 : "\\223", std.conv.octal!224 : "\\224", 98 std.conv.octal!225 : "\\225", std.conv.octal!226 : "\\226", 99 std.conv.octal!227 : "\\227", std.conv.octal!230 : "\\230", 100 std.conv.octal!231 : "\\231", std.conv.octal!232 : "\\232", 101 std.conv.octal!233 : "\\233", std.conv.octal!234 : "\\234", 102 std.conv.octal!235 : "\\235", std.conv.octal!236 : "\\236", 103 std.conv.octal!237 : "\\237", std.conv.octal!240 : "\\240", 104 std.conv.octal!241 : "\\241", std.conv.octal!242 : "\\242", 105 std.conv.octal!243 : "\\243", std.conv.octal!244 : "\\244", 106 std.conv.octal!245 : "\\245", std.conv.octal!246 : "\\246", 107 std.conv.octal!247 : "\\247", std.conv.octal!250 : "\\250", 108 std.conv.octal!251 : "\\251", std.conv.octal!252 : "\\252", 109 std.conv.octal!253 : "\\253", std.conv.octal!254 : "\\254", 110 std.conv.octal!255 : "\\255", std.conv.octal!256 : "\\256", 111 std.conv.octal!257 : "\\257", std.conv.octal!260 : "\\260", 112 std.conv.octal!261 : "\\261", std.conv.octal!262 : "\\262", 113 std.conv.octal!263 : "\\263", std.conv.octal!264 : "\\264", 114 std.conv.octal!265 : "\\265", std.conv.octal!266 : "\\266", 115 std.conv.octal!267 : "\\267", std.conv.octal!270 : "\\270", 116 std.conv.octal!271 : "\\271", std.conv.octal!272 : "\\272", 117 std.conv.octal!273 : "\\273", std.conv.octal!274 : "\\274", 118 std.conv.octal!275 : "\\275", std.conv.octal!276 : "\\276", 119 std.conv.octal!277 : "\\277", std.conv.octal!300 : "\\300", 120 std.conv.octal!301 : "\\301", std.conv.octal!302 : "\\302", 121 std.conv.octal!303 : "\\303", std.conv.octal!304 : "\\304", 122 std.conv.octal!305 : "\\305", std.conv.octal!306 : "\\306", 123 std.conv.octal!307 : "\\307", std.conv.octal!310 : "\\310", 124 std.conv.octal!311 : "\\311", std.conv.octal!312 : "\\312", 125 std.conv.octal!313 : "\\313", std.conv.octal!314 : "\\314", 126 std.conv.octal!315 : "\\315", std.conv.octal!316 : "\\316", 127 std.conv.octal!317 : "\\317", std.conv.octal!320 : "\\320", 128 std.conv.octal!321 : "\\321", std.conv.octal!322 : "\\322", 129 std.conv.octal!323 : "\\323", std.conv.octal!324 : "\\324", 130 std.conv.octal!325 : "\\325", std.conv.octal!326 : "\\326", 131 std.conv.octal!327 : "\\327", std.conv.octal!330 : "\\330", 132 std.conv.octal!331 : "\\331", std.conv.octal!332 : "\\332", 133 std.conv.octal!333 : "\\333", std.conv.octal!334 : "\\334", 134 std.conv.octal!335 : "\\335", std.conv.octal!336 : "\\336", 135 std.conv.octal!337 : "\\337", std.conv.octal!340 : "\\340", 136 std.conv.octal!341 : "\\341", std.conv.octal!342 : "\\342", 137 std.conv.octal!343 : "\\343", std.conv.octal!344 : "\\344", 138 std.conv.octal!345 : "\\345", std.conv.octal!346 : "\\346", 139 std.conv.octal!347 : "\\347", std.conv.octal!350 : "\\350", 140 std.conv.octal!351 : "\\351", std.conv.octal!352 : "\\352", 141 std.conv.octal!353 : "\\353", std.conv.octal!354 : "\\354", 142 std.conv.octal!355 : "\\355", std.conv.octal!356 : "\\356", 143 std.conv.octal!357 : "\\357", std.conv.octal!360 : "\\360", 144 std.conv.octal!361 : "\\361", std.conv.octal!362 : "\\362", 145 std.conv.octal!363 : "\\363", std.conv.octal!364 : "\\364", 146 std.conv.octal!365 : "\\365", std.conv.octal!366 : "\\366", 147 std.conv.octal!367 : "\\367", std.conv.octal!370 : "\\370", 148 std.conv.octal!371 : "\\371", std.conv.octal!372 : "\\372", 149 std.conv.octal!373 : "\\373", std.conv.octal!374 : "\\374", 150 std.conv.octal!375 : "\\375", std.conv.octal!376 : "\\376", 151 std.conv.octal!377 : "\\377", 152 ]; 153 154 bool has_legal_chars(string s) 155 { 156 foreach (c; s) 157 { 158 if (c !in _legalchars) 159 return false; 160 } 161 162 return true; 163 } 164 165 string cookie_quote(string input) 166 { 167 char[] result = new char[input.length * 4]; 168 uint lastid = 0; 169 bool usedspecial = false; 170 171 foreach (c; input) 172 { 173 174 if (c !in _legalchars) 175 { 176 // Not legal char 177 usedspecial = true; 178 179 if (cast(uint) c in _cookiechars) 180 { 181 // We got encoding for it 182 result[lastid .. lastid + 4] = _cookiechars[cast(uint) c]; 183 lastid += 4; 184 } 185 else 186 { 187 // Not in legalchars, not in the cookiechars either... just append it, 188 // but the string is already marked as "special" so it will be quoted 189 result[lastid] = c; 190 ++lastid; 191 } 192 193 } 194 else 195 { 196 result[lastid] = c; 197 ++lastid; 198 } 199 } 200 201 result.length = lastid; 202 203 // Put quotes around the string if we used some encoded character 204 return usedspecial ? '"' ~ to!string(result) ~ '"' : to!string(result); 205 } 206 207 // XXX Optimize this, use a string Appender or a char[], etc... 208 string cookie_unquote(string quoted_value) 209 { 210 string result = ""; 211 212 // If there aren't any doublequotes, then there can't special chars. See RFC 2109 213 if (quoted_value.length < 2) 214 return quoted_value; 215 216 //if (quoted_value[0] != '"' || quoted_value[$-1] != '"') 217 //return quoted_value; 218 219 // Remove the "..." 220 string unquoted = quoted_value; //[1..$-1]; 221 222 // Check for special chars \012 => \n, \" => " 223 size_t i = 0; 224 auto n = unquoted.length; 225 size_t Omatch, Qmatch; 226 size_t j, k; 227 228 while (i >= 0 && i < n) 229 { 230 import std.regex; 231 232 auto ocaptures = match(unquoted[i .. $], _OctalPatt).captures; 233 if (ocaptures.length == 0) 234 Omatch = -1; 235 else 236 Omatch = std..string.indexOf(unquoted[i .. $], ocaptures[0]); 237 238 auto qcaptures = match(unquoted[i .. $], _QuotePatt).captures; 239 if (qcaptures.length == 0) 240 Qmatch = -1; 241 else 242 Qmatch = std..string.indexOf(unquoted[i .. $], qcaptures[0]); 243 244 if (Omatch == -1 && Qmatch == -1) 245 { // Neither matched 246 result ~= unquoted[i .. $]; 247 break; 248 } 249 250 j = -1; 251 k = -1; 252 if (Omatch != -1) 253 j = Omatch + i; 254 if (Qmatch != -1) 255 k = Qmatch + i; 256 257 if (Qmatch != -1 && ((Omatch == -1) || (k < j))) // QuotePatt matched 258 { 259 result ~= unquoted[i .. k] ~ unquoted[k + 1]; 260 i = k + 2; 261 } 262 else // OctalPatt matched 263 { 264 result ~= unquoted[i .. j]; 265 result ~= cast(char) strtoul(toStringz(unquoted[j + 1 .. j + 4]), null, 266 8); 267 i = j + 4; 268 } 269 } 270 271 return result; 272 } 273 274 // XXX: Capture possible exceptions 275 // Convert a (possibly encoded) client "Cookie: " HTTP header into an 276 // list of Cookie objects. This function expects the "Cookie: " or 277 // "Set-Cookie: " header name to be already removed 278 Cookie[string] parseCookie(string header) 279 { 280 /// if cookie string is not null 281 if (header is null) 282 return null; 283 284 /// parse the cookies 285 Cookie[string] result; 286 287 string[] cookie_parts = header.split(";"); 288 289 foreach (idx, part; cookie_parts) 290 { 291 auto cookie = new Cookie(); 292 string[] keyvalue = part.split("="); 293 if (keyvalue.length != 2) // WTF!? 294 continue; 295 296 string key = keyvalue[0].strip; 297 298 if (key[0] == '$') 299 continue; 300 301 string quoted_value = keyvalue[1]; 302 303 string lkey = toLower(key); 304 305 if (lkey in RESERVED_PARAMS) 306 { 307 // Ignore if we've not set the name yet 308 if (!cookie.is_name_set) 309 continue; 310 311 cookie.set(lkey, cookie_unquote(quoted_value)); 312 313 // Add the cookie if this is the last token 314 if (idx == cookie_parts.length - 1) 315 { 316 result[lkey] = cookie; 317 } 318 } 319 320 // It is a name-value, not a reserved param 321 else 322 { 323 cookie.set(key, cookie_unquote(quoted_value)); 324 result[key] = cookie; 325 } 326 } 327 328 return result; 329 } 330 331 // ================================= 332 // Cookie class 333 // ================================= 334 class Cookie 335 { 336 enum DEFAULT_HEADER = "Set-Cookie: "; 337 338 static bool is_reserved_key(string key) 339 { 340 return (toLower(key) in RESERVED_PARAMS) != null; 341 } 342 343 this(string cname, string cvalue, string[string] cparams) 344 { 345 this(cname, cvalue); 346 params(cparams); 347 348 } 349 350 this(string cname, string cvalue) 351 { 352 set(cname, cvalue); 353 this(); 354 } 355 356 this() 357 { 358 initialize_cookieparams(); 359 } 360 361 void set(string name, string value) 362 in 363 { 364 assert(name != null); 365 //assert(value != null); 366 } 367 body 368 { 369 string lname = toLower(name); 370 if (lname in RESERVED_PARAMS) 371 { 372 _cookieparams[lname] = value; 373 } 374 else 375 { 376 // Check that all the chars in the name are legal 377 if (!has_legal_chars(name)) 378 throw new CookieException("Illegal name '" ~ name ~ "' has ilegal chars"); 379 380 _name = name; 381 _value = value; 382 _quoted_value = cookie_quote(value); 383 } 384 } 385 386 @property string name() 387 { 388 if (_name is null) 389 throw new CookieException("Cookie name not set"); 390 return _name; 391 } 392 393 @property void name(string newname) 394 { 395 if (!has_legal_chars(name)) 396 throw new CookieException("The name '" ~ name ~ "' has ilegal chars"); 397 398 _name = newname; 399 } 400 401 @property string value() 402 { 403 if (_value is null) 404 throw new CookieException("Cookie value not set"); 405 return _value; 406 } 407 408 @property void value(string newvalue) 409 { 410 _value = newvalue; 411 _quoted_value = cookie_quote(_value); 412 } 413 414 @property string quoted_value() 415 { 416 if (_quoted_value is null) 417 throw new CookieException("Cookie quoted_value not set"); 418 return _quoted_value; 419 } 420 421 @property void quoted_value(string newvalue) 422 { 423 _quoted_value = newvalue; 424 _value = cookie_unquote(_quoted_value); 425 } 426 427 @property string[string] params() 428 { 429 return _cookieparams; 430 } 431 432 @property void params(string[string] newparams) 433 { 434 // Join the dicts 435 foreach (key, value; newparams) 436 { 437 if (!has_legal_chars(key)) 438 throw new CookieException("The key '" ~ key ~ "' has ilegal chars"); 439 440 string lkey = toLower(key); 441 if (lkey in RESERVED_PARAMS) 442 _cookieparams[lkey] = value; 443 else 444 throw new CookieException("Wrong cookie parameter '" ~ key ~ "'"); 445 } 446 } 447 448 @property bool is_name_set() 449 { 450 return !(_name is null); 451 } 452 453 @property bool is_value_set() 454 { 455 return !(_value is null); 456 } 457 458 string get(const string key) 459 { 460 auto res = get(key, null); 461 462 return res; 463 } 464 465 string get(const string key, const string _default) 466 { 467 if (key == _name) 468 return _value; 469 470 string lkey = toLower(key); 471 if (lkey in _cookieparams) 472 return _cookieparams[lkey]; 473 474 return _default; 475 } 476 477 void setkey(string key, string _value) 478 { 479 string lkey = toLower(key); 480 if (lkey !in RESERVED_PARAMS && key != _name) 481 throw new CookieException( 482 "Wrong cookie index '" ~ key ~ "'. Use 'name', 'value', 'quoted_value' or valid cookie parameter (see RFC 2109)"); 483 484 if (key == _name) 485 name(_value); 486 else 487 _cookieparams[lkey] = _value; 488 } 489 490 string output(string[] attrs, string header) 491 { 492 string result = header ~ _name ~ "=" ~ _quoted_value; 493 auto paramkeys = _cookieparams.keys; 494 495 foreach (param, value; _cookieparams) 496 { 497 // Add the cooieparam only if is in the user specified attrs and is not empty 498 if (std.algorithm.countUntil(attrs, param) != -1 && value.length > 0) 499 result ~= ";" ~ param ~ "=" ~ value; 500 } 501 return result; 502 } 503 504 string output(string header) 505 { 506 return output(_cookieparams.keys, header); 507 } 508 509 string output() 510 { 511 return output(_cookieparams.keys, DEFAULT_HEADER); 512 } 513 514 override string toString() 515 { 516 return output(); 517 } 518 519 private: 520 string _name = null; 521 string _quoted_value = null; 522 string _value = null; 523 string _decodedvalue = null; 524 string[string] _cookieparams = null; 525 526 protected: 527 void initialize_cookieparams() 528 { 529 foreach (key, value; RESERVED_PARAMS) 530 _cookieparams[key] = ""; 531 } 532 533 } 534 535 unittest 536 { 537 auto cookies = parseCookie("PHPSESSID=dh5vvosj68hv1raprertnku6s7; LBN=node2; Hm_lvt_9e6c6312b8b64e7e38b0b84c12642b96=1461739077,1461897406,1462760128,1462760222; Hm_lpvt_9e6c6312b8b64e7e38b0b84c12642b96=1463122691; __utmt=1; __utma=233165215.1997191855.1458546658.1463106445.1463122691.5; __utmb=233165215.1.10.1463122691; __utmc=233165215; __utmz=233165215.1458546658.1.1.utmcsr=account.start.wang|utmccn=(referral)|utmcmd=referral|utmcct=/register"); 538 import std.experimental.logger; 539 540 assert(cookies.get("PHPSESSID", null).value == "dh5vvosj68hv1raprertnku6s7"); 541 assert(cookies["LBN"].value == "node2"); 542 } 543 544 unittest 545 { 546 //generate cookie 547 auto cookie = new Cookie("PHPSESSID", "dh5vvosj68hv1raprertnku6s7"); 548 //assert(); 549 import std.experimental.logger; 550 551 assert(cookie.output == "Set-Cookie: PHPSESSID=dh5vvosj68hv1raprertnku6s7"); 552 cookie.params = ["expires" : "Fri, 13 May 2016 17:44:17 GMT", "path" : "/", 553 "domain" : "putao.com", "secure" : "true", "httponly" : "false"]; 554 555 assert( cookie.output == "Set-Cookie: PHPSESSID=dh5vvosj68hv1raprertnku6s7;expires=Fri, 13 May 2016 17:44:17 GMT;domain=putao.com;path=/;secure=true;httponly=false"); 556 557 558 }